package me.cabbit.digitone.gui;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

import javax.swing.JPanel;
import javax.swing.SwingWorker;

import me.cabbit.digitone.tones.Screentone;
import me.cabbit.digitone.tones.ScreentoneListener;


class AsyncRenderer extends SwingWorker<BufferedImage, Integer> implements ScreentoneListener {

	private Screentone screentone;
	
	public AsyncRenderer(Screentone st) {
		this.screentone = st;
		this.screentone.addScreentoneListener(this);
	}
	
	@Override
	protected BufferedImage doInBackground() {
		BufferedImage bi = new BufferedImage(this.screentone.getWidth(), this.screentone.getHeight(), BufferedImage.TYPE_INT_ARGB);
		Graphics2D target = bi.createGraphics();
		this.screentone.render(target);
		target.dispose();
		
		this.screentone.removeScreentoneListener(this);
		return bi;
	}

	@Override
	public void propertyChanged() {}

	@Override
	public void buildProgressChanged(int progress) {
		this.setProgress(progress);
	}
}

/**
 * A panel that display {@link Screentone} objects. To display one
 * set it by using {@link #setScreentone(Screentone)}. By design
 * a screentone is not refreshed (recalculated) until it's told to do 
 * so. Zooming and panning only use the latest created screentone.
 * To force generation of a new one, use {@link #forceRender()}
 * 
 * @author Arkaid
 *
 */
class ScreentoneDisplay extends JPanel  implements MouseListener, MouseMotionListener, MouseWheelListener, PropertyChangeListener {
	private static final long serialVersionUID = -6893202111848751129L;

	//------------------------------------------------------------------//
	//--------------------------- properties ---------------------------//
	//------------------------------------------------------------------//

	private BufferedImage lastRender;
	private AsyncRenderer renderer;
	
	/** Transform representing zoom and scale */
	private AffineTransform transform;
	
	/** Used to track mouse position during panning (X) */
	private int lastX;

	/** Used to track mouse position during panning (Y) */
	private int lastY;

	/** A flag that indicates the screentone must be re-rendered */
	private boolean forceScreentoneRender;
	
	/** Screentone to display */
	private Screentone screentone;
	
	/** Window this display belongs to */
	private MainWindow mainWnd;
	
	//------------------------------------------------------------------//
	//----------------------------- methods ----------------------------//
	//------------------------------------------------------------------//

	/**
	 * Constructor. Set up background color and attach listeners
	 * @param mainWnd Window this control belongs to
	 */
	public ScreentoneDisplay(MainWindow mainWnd) 
	{
		this.mainWnd = mainWnd;
		
		this.setBackground(Color.DARK_GRAY);
		
		forceRender();
		resetTransforms();
		
		this.addMouseMotionListener(this);
		this.addMouseListener(this);
		this.addMouseWheelListener(this);
	}

	/**
	 * Tell the display to re-render a currently displaying screentone
	 * If this is not called, parameter changes to the screentone
	 * do not reflect in the display
	 */
	public void forceRender()
	{
		this.forceScreentoneRender = true;
	}
	
	/** Resets pan and zoom transformations */
	public void resetTransforms() 
	{		
		this.transform = new AffineTransform();
	}
	
	/**
	 * Sets the screentone to display
	 * @param st Screentone to display
	 */
	public void setScreentone(Screentone st)
	{
		this.screentone = st;
		forceRender();
	}
	
	/**
	 * Creates a {@link BufferedImage} from the current screentone
	 * without any transformations (like zoom and pan). Useful if 
	 * you want to save the currently displaying image to a file
	 * @return An image with the currently displaying result
	 */
	public BufferedImage createBufferedImage()
	{
		return this.lastRender;
	}
	
	@Override
	public void paint(Graphics g) {
	
		
		super.paint(g);
		
		if (this.screentone == null)
			return;

		if (this.forceScreentoneRender)
		{
			this.mainWnd.statusBar.setStatus(this.mainWnd.strings.get("STATUS_RENDERING"));
			this.renderer = new AsyncRenderer(this.screentone);
			renderer.addPropertyChangeListener(this);
			renderer.execute();
			this.forceScreentoneRender = false;
		}
		
		Graphics2D g2 = (Graphics2D)g;
		g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);

		int stw = this.screentone.getWidth();
		int sth = this.screentone.getHeight();

		//clear to the control's background color
        g2.setColor(getBackground());
        g2.fillRect(0, 0, getWidth(), getHeight());
        
        //get default transform
        AffineTransform t = g2.getTransform();
               
        //center on screen
        Point center = new Point();
        center.x = (int) ((this.getWidth() - stw * this.transform.getScaleX()) / 2);
        center.y = (int) ((this.getHeight() - sth * this.transform.getScaleY()) / 2);
        t.translate(center.x, center.y);

        //apply zoom and translate
        t.concatenate(this.transform);

        //apply transformations
        g2.setTransform(t);

		g2.drawImage(this.lastRender, 0, 0, this);
	}

	//------------------------------------------------------------------//
	//------------------------ MouseWheelListener ----------------------//
	//------------------------------------------------------------------//

	@Override
	public void mouseWheelMoved(MouseWheelEvent event) 
	{
		double scaleFactor = 0.8;
		
		//scale back and forth
		if (event.getWheelRotation() > 0)
			this.transform.scale(scaleFactor, scaleFactor);
		else
			this.transform.scale(1/scaleFactor, 1/scaleFactor);
		
		//update the status bar
		this.mainWnd.statusBar.setZoom(this.transform.getScaleX());
		
		this.repaint();		
	}
	
	//------------------------------------------------------------------//
	//----------------------- MouseMotionListener ----------------------//
	//------------------------------------------------------------------//

	@Override
	public void mouseDragged(MouseEvent event) 
	{
		//pan
		double scale = this.transform.getScaleX();
		
		double dx = (event.getX() - this.lastX) / scale;
		double dy = (event.getY() - this.lastY) / scale; 
		this.transform.translate(dx, dy);
		
		this.lastX = event.getX();
		this.lastY = event.getY();
		
		this.repaint();
	}

	@Override
	public void mouseMoved(MouseEvent event) {}
	
	//------------------------------------------------------------------//
	//--------------------------- MouseListener ------------------------//
	//------------------------------------------------------------------//

	@Override
	public void mouseClicked(MouseEvent event) {}

	@Override
	public void mouseEntered(MouseEvent event) {}

	@Override
	public void mouseExited(MouseEvent event) {	}

	@Override
	public void mousePressed(MouseEvent event) 
	{
		this.lastX = event.getX();
		this.lastY = event.getY();
	}

	@Override
	public void mouseReleased(MouseEvent event) {}

	@Override
	public void propertyChange(PropertyChangeEvent event) {
		
		String property = event.getPropertyName(); 
		
		if (property.equals("progress")) 
            this.mainWnd.statusBar.setProgress((Integer)event.getNewValue());
        
		else if (property.equals("state") && event.getNewValue() == SwingWorker.StateValue.DONE)
		{
			try {
				this.lastRender = this.renderer.get();
			} catch (Exception e) {
				e.printStackTrace();
			}
			repaint();
			this.mainWnd.statusBar.setStatus(this.mainWnd.strings.get("STATUS_READY"));
		}
	}	
}
